/*
 * gomacro - A Go interpreter with Lisp-like macros
 *
 * Copyright (C) 2017-2019 Massimiliano Ghilardi
 *
 *     This Source Code Form is subject to the terms of the Mozilla Public
 *     License, v. 2.0. If a copy of the MPL was not distributed with this
 *     file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 *
 * val.go
 *
 *  Created on May 27, 2017
 *      Author Massimiliano Ghilardi
 */

package untyped

import (
	"fmt"
	"go/constant"
	"go/token"
	"go/types"
	"math/big"
	"strings"

	"github.com/cosmos72/gomacro/base/output"
)

// untyped value
type Val struct {
	Kind Kind // default type
	Val  constant.Value
}

func GoUntypedToKind(gkind types.BasicKind) Kind {
	var kind Kind
	switch gkind {
	case types.UntypedBool:
		kind = Bool
	case types.UntypedInt:
		kind = Int
	case types.UntypedRune:
		kind = Rune
	case types.UntypedFloat:
		kind = Float
	case types.UntypedComplex:
		kind = Complex
	case types.UntypedString:
		kind = String
	case types.UntypedNil:
		kind = None
	default:
		output.Errorf("unsupported types.BasicKind: %v", gkind)
	}
	return kind
}

func (val *Val) Marshal() string {
	return Marshal(val.Kind, val.Val)
}

func Marshal(kind Kind, val constant.Value) string {
	// untyped constants have arbitrary precision... they may overflow integers
	var s string
	switch kind {
	case None:
		s = "nil"
	case Bool:
		if constant.BoolVal(val) {
			s = "bool:true"
		} else {
			s = "bool:false"
		}
	case Int:
		s = fmt.Sprintf("int:%s", val.ExactString())
	case Rune:
		s = fmt.Sprintf("rune:%s", val.ExactString())
	case Float:
		s = fmt.Sprintf("float:%s", val.ExactString())
	case Complex:
		s = fmt.Sprintf("complex:%s:%s", constant.Real(val).ExactString(), constant.Imag(val).ExactString())
	case String:
		s = fmt.Sprintf("string:%s", constant.StringVal(val))
	}
	return s
}

func UnmarshalVal(marshalled string) *Val {
	kind, val := Unmarshal(marshalled)
	return &Val{kind, val}
}

func Unmarshal(marshalled string) (Kind, constant.Value) {
	var skind, str string
	if sep := strings.IndexByte(marshalled, ':'); sep >= 0 {
		skind = marshalled[:sep]
		str = marshalled[sep+1:]
	} else {
		skind = marshalled
	}

	var kind Kind
	var val constant.Value
	switch skind {
	case "bool":
		kind = Bool
		if str == "true" {
			val = constant.MakeBool(true)
		} else {
			val = constant.MakeBool(false)
		}
	case "int":
		kind = Int
		val = constant.MakeFromLiteral(str, token.INT, 0)
	case "rune":
		kind = Rune
		val = constant.MakeFromLiteral(str, token.INT, 0)
	case "float":
		kind = Float
		val = unmarshalFloat(str)
	case "complex":
		kind = Complex
		if sep := strings.IndexByte(str, ':'); sep >= 0 {
			re := unmarshalFloat(str[:sep])
			im := unmarshalFloat(str[sep+1:])
			val = constant.BinaryOp(constant.ToComplex(re), token.ADD, constant.MakeImag(im))
		} else {
			val = constant.ToComplex(unmarshalFloat(str))
		}
	case "string":
		kind = String
		val = constant.MakeString(str)
	case "nil":
		kind = None
	default:
		kind = None
	}
	return kind, val
}

// generalization of constant.MakeFromLiteral, accepts the fractions generated by
// constant.Value.ExactString() for floating-point values
func unmarshalFloat(str string) constant.Value {
	if sep := strings.IndexByte(str, '/'); sep >= 0 {
		x := constant.MakeFromLiteral(str[:sep], token.FLOAT, 0)
		y := constant.MakeFromLiteral(str[sep+1:], token.FLOAT, 0)
		return constant.BinaryOp(x, token.QUO, y)
	}
	return constant.MakeFromLiteral(str, token.FLOAT, 0)
}

func (lit *Val) BigInt() (*big.Int, error) {
	val := lit.Val
	switch lit.Kind {
	case Int, Rune:
		if i, ok := constant.Int64Val(val); ok {
			return big.NewInt(i), nil
		}
		if bi, ok := new(big.Int).SetString(val.ExactString(), 10); ok {
			return bi, nil
		}
	}
	return nil, output.MakeRuntimeError("cannot convert untyped %s to math/big.Int: %v", lit.Kind, lit.Val)
}

func (lit *Val) BigRat() (*big.Rat, error) {
	val := lit.Val
	switch lit.Kind {
	case Int, Rune:
		if i, ok := constant.Int64Val(val); ok {
			return big.NewRat(i, 1), nil
		}
		fallthrough
	case Float:
		if br, ok := new(big.Rat).SetString(val.ExactString()); ok {
			return br, nil
		}
	}
	return nil, output.MakeRuntimeError("cannot convert untyped %s to math/big.Rat: %v", lit.Kind, lit.Val)
}
